package de.lmu.ifi.pst.uwejsf.framework.process;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.faces.event.ActionEvent;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.ListDataModel;
import javax.faces.model.SelectItem;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.apache.commons.beanutils.BeanUtils;

import de.lmu.ifi.pst.uwejsf.framework.AbstractUWEContextFactory;
import de.lmu.ifi.pst.uwejsf.framework.ExpressionHelpers;
import de.lmu.ifi.pst.uwejsf.framework.NavigationNodeBean;
import de.lmu.ifi.pst.uwejsf.framework.RootBeanScriptContext;
import de.lmu.ifi.pst.uwejsf.framework.UWEHelpers;
import de.lmu.ifi.pst.uwejsf.framework.UWEJSFRuntimeException;
import de.lmu.ifi.pst.uwejsf.framework.UWEScriptHelper;

public abstract class ProcessClassBean extends NavigationNodeBean {

	protected String activityName;
	protected String userActionName;
	protected Map<String, ListDataModel> dataModelMap = new HashMap<String, ListDataModel>();
	protected Map<String, Object> rowDataMap = new HashMap<String, Object>();
	protected Map<String, Integer> rowIndexMap = new HashMap<String, Integer>();

	protected Map<Class<?>, Map<Integer, Object>> optionMap = new HashMap<Class<?>, Map<Integer, Object>>();

	public ProcessClassBean(String activityName, String userActionName) {
		this.activityName = activityName;
		this.userActionName = userActionName;
	}

	public ProcessClassBean(String activityName) {
		this(activityName, null);
	}

	public String getActivityName() {
		return activityName;
	}

	public void setActivityName(String activityName) {
		this.activityName = activityName;
	}

	public String getUserActionName() {
		return userActionName;
	}

	public void setUserActionName(String userActionName) {
		this.userActionName = userActionName;
	}

	public ProcessActivityBean getActivityBean() {
		return (ProcessActivityBean) ExpressionHelpers
				.resolveValue("#{activity" + activityName + "}");
	}

	public String submit() {

		return "to_"
				+ getUWENavigator().handleSubmit(activityName, userActionName);
	}

	public void handleSubmitAction(ActionEvent ev) {
		String actionName = (String) ev.getComponent().getAttributes().get(
				"actionName");
		getActivityBean().storeObject("useraction_name", actionName);
		handleDataModels();

	}

	private void handleDataModels() {
		for (String key : dataModelMap.keySet()) {
			int rowIndex = dataModelMap.get(key).getRowIndex();
			rowIndexMap.put(key, rowIndex);
			if (rowIndex > -1)
				rowDataMap.put(key, dataModelMap.get(key).getRowData());
			else
				rowDataMap.put(key, null);

		}
	}

	/**
	 * Used as change listener for components with submitOnChange=true.
	 * 
	 * @param ev
	 */
	public void handleSubmitOnChange(ValueChangeEvent ev) {
		if (hasValueChanged(ev.getOldValue(), ev.getNewValue()))
			getUWENavigator().registerSubmitOrder(this);

	}

	protected boolean hasValueChanged(Object oldValue, Object newValue) {
		if (oldValue == null || newValue == null)
			return newValue != oldValue;

		if (newValue instanceof String)
			return !oldValue.toString().equals((String) newValue);
		else
			return !oldValue.equals(newValue);

	}

	protected List<Object> createSelectItems(List<?> options,
			String labelExpression) {
		List<Object> items = new ArrayList<Object>();
		ScriptEngine engine = AbstractUWEContextFactory.getCurrentInstance()
				.getScriptEngineByName("ognl");
		RootBeanScriptContext scriptContext = new RootBeanScriptContext(null);
		UWEScriptHelper scriptHelper = new UWEScriptHelper(
				AbstractUWEContextFactory.getCurrentInstance());
		scriptContext.getBindings(ScriptContext.ENGINE_SCOPE).put("uweHelper",
				scriptHelper);

		for (Object o : options) {

			String label = null;
			if (labelExpression != null && !labelExpression.trim().isEmpty()) {
				scriptContext.setRootBean(o);
				try {
					label = (String) engine
							.eval(labelExpression, scriptContext);
				} catch (ScriptException e) {
					throw new ProcessDataflowException(
							"Unable to evaluate expression: " + labelExpression);
				}
			} else {
				label = o.toString();
			}
			items.add(new SelectItem(Integer.toString(o.hashCode()), label));

		}
		return items;
	}

	protected Object convertToSelectionTarget(Object o) {
		if (o == null)
			return null;
		if (o instanceof Enum) {
			return ((Enum<?>) o).name();
		}
		Object result = null;
		if (o instanceof List) {
			List<?> sourceList = (List<?>) o;
			List<String> resultList = new ArrayList<String>();
			for (Object srcObject : sourceList) {
				resultList.add(Integer.toString(srcObject.hashCode()));
			}
			result = resultList;
		} else {
			result = Integer.toString(o.hashCode());
		}
		return result;
	}

	public <T> List<T> convertFromSelectionTarget_list(Object selTarget,
			Class<T> elementClass) {

		List<T> result = new ArrayList<T>();
		if (selTarget == null)
			return result;
		List<?> sourceList = (List<?>) selTarget;
		for (Object o : sourceList) {
			result.add(convertFromSelectionTarget(o, elementClass));
		}
		return result;

	}

	/**
	 * Retrieves the actual value object from the value set in the JSF list.
	 * 
	 * @param <T>
	 * @param selTarget
	 * @param expectedClass
	 * @return
	 */
	public <T> T convertFromSelectionTarget(Object selTarget,
			Class<T> expectedClass) {

		if (selTarget == null)
			return null;
		if (expectedClass.isAssignableFrom(selTarget.getClass()))
			return expectedClass.cast(selTarget);

		if (expectedClass.isEnum()) {
			for (T enumConstant : expectedClass.getEnumConstants()) {
				if (((Enum<?>) enumConstant).name().equals(selTarget))
					return enumConstant;
			}
		}
		int hashCode = 0;
		// ok,
		if (selTarget instanceof String)
			hashCode = Integer.parseInt((String) selTarget);
		else
			hashCode = (Integer) selTarget;

		Map<Integer, Object> mapForClass = optionMap.get(expectedClass);
		Object o = mapForClass.get(hashCode);
		return expectedClass.cast(o);

	}

	protected void registerOptions(List<Object> options, Class<?> cl) {
		Map<Integer, Object> mapForClass = optionMap.get(cl);
		if (mapForClass == null) {
			mapForClass = new HashMap<Integer, Object>();
			optionMap.put(cl, mapForClass);
		}
		for (Object o : options) {
			mapForClass.put(o.hashCode(), o);
		}
	}

	protected ListDataModel createDataModel(Object data) {
		if (data instanceof ListDataModel)
			return (ListDataModel) data;

		if (!(data instanceof List))
			throw new ProcessDataflowException(
					"Trying to creat ListDataModel from non-list data: "
							+ data.toString());
		return new ListDataModel((List<?>) data);
	}

	public abstract Map<String, Class<?>> getPropertyTypes();

	public Map<String, ?> getUserInput() {
		Map<String, Object> result = new HashMap<String, Object>();
		for (String propName : getPropertyTypes().keySet()) {
			try {
				result.put(propName, BeanUtils.getProperty(this, propName));
			} catch (Exception e) {
				throw new UWEJSFRuntimeException("Couldn't retrieve property "
						+ propName + " of ProcessBean " + getClass().getName(),
						e);
			}

		}

		return result;
	}

	private Method selectSetter(Method[] methods, String propName, Class<?> pt) {

		String methodName = "set" + UWEHelpers.firstToUpper(propName);
		for (Method m : methods) {
			if (m.getName().equals(methodName)
					&& m.getParameterTypes().length == 1
					&& m.getParameterTypes()[0].isAssignableFrom(pt))
				return m;
		}
		return null;
	}

	/**
	 * checks whether the given property is a selection property
	 * 
	 * @param propName
	 * @return
	 */
	private boolean isOptionsProperty(Method[] methods, String propName) {

		String gName = "get" + UWEHelpers.firstToUpper(propName) + "_options";

		for (Method m : methods) {
			if (m.getName().equals(gName))
				return true;
		}
		return false;
	}

	/**
	 * Clears the properties of this process class. Selection properties are not
	 * cleared.
	 * 
	 */
	// TODO: think about having a tag "clear" for ProcessClass
	public void clearProperties() {
		Method[] methods = getClass().getMethods();
		Map<String, Class<?>> propTypes = getPropertyTypes();
		for (String propName : propTypes.keySet()) {
			if (!isOptionsProperty(methods, propName)) {
				Class<?> pt = propTypes.get(propName);
				try {
					Method setter = selectSetter(methods, propName, pt);

					Object value = UWEHelpers.getDefaultValueForType(pt);

					setter.invoke(this, value);
				} catch (Exception e) {
					throw new UWEJSFRuntimeException(
							"Unable to call setter for property " + propName
									+ " in ProcessClassBean "
									+ getClass().getName(), e);
				}
			}

		}
	}

}
